﻿using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Security;

namespace Nebular.ExpenseManagement.WebApplication
{
    /// <summary>
    /// Enables multi tenant user pools by extending the standard SqlMembershipProvider
    /// simply by dropping a part of the request url in front of the username and email
    /// adresses. The url part is specified by a regular expression group named 'tenant' and
    /// could e.g. match a subsite name.
    /// For instance a request to 'https://maersk.expense.com/' by user name 'moeller'
    /// could translate to maersk.moeller in the underlying provider.
    /// </summary>
    /// <remarks>The providerUserKey should still be unique across tenants (but could be 
    /// wrapped in a future version)</remarks>
    /// <example><c>
    /// <![CDATA[<add name="AspNetSqlMembershipProvider" 
    /// type="Nebular.ExpenseManagement.WebApplication.MultitenantSqlMembershipProvider, Nebular.ExpenseManagement.WebApplication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2d09fee4f0fa0c6d"
    /// connectionStringName="ApplicationServices" enablePasswordRetrieval="false" 
    /// enablePasswordReset="true" requiresQuestionAndAnswer="false" 
    /// requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" 
    /// minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" 
    /// passwordAttemptWindow="10" passwordStrengthRegularExpression="" 
    /// applicationName="/" tenantRegularExpression="^(?:http[s]?://)(?'tenant'[^/^:^\.]+)"/>]]>
    /// </c></example>
    public class MultitenantSqlMembershipProvider
        : SqlMembershipProvider
    {
        readonly ITenantProvider _tenantprovider =
            MvcApplication.Container != null
            ? MvcApplication.Container.GetService<ITenantProvider>()
            : new TenantProvider("localhost");

        public string Tenant
        {
            get { return _tenantprovider.Tenant; }
        }
        protected override void OnValidatingPassword(ValidatePasswordEventArgs e)
        {            
            var alt = new ValidatePasswordEventArgs(
                AsSingleTenant(e.UserName),
                e.Password,
                e.IsNewUser);
            base.OnValidatingPassword(alt);
            e.FailureInformation = alt.FailureInformation;
            e.Cancel = alt.Cancel;
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            return base.ChangePassword(AsMultiTenant(username), oldPassword, newPassword);
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            return base.ChangePasswordQuestionAndAnswer(
                AsMultiTenant(username), 
                password, 
                newPasswordQuestion, 
                newPasswordAnswer);
        }

        public override MembershipUser CreateUser(
            string username, 
            string password, 
            string email, 
            string passwordQuestion, 
            string passwordAnswer, 
            bool isApproved, 
            object providerUserKey, 
            out MembershipCreateStatus status)
        {
            var user = base.CreateUser(
                AsMultiTenant(username),
                password,
                AsMultiTenant(email),
                passwordQuestion,
                passwordAnswer,
                isApproved,
                providerUserKey,
                out status);
            return (user != null)
                ? AsSingleTenant(user)
                : null;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            return base.DeleteUser(AsMultiTenant(username), deleteAllRelatedData);
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            return AsSingleTenant(base.FindUsersByEmail(AsMultiTenant(emailToMatch), pageIndex, pageSize, out totalRecords));
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            return AsSingleTenant(base.FindUsersByName(AsMultiTenant(usernameToMatch), pageIndex, pageSize, out totalRecords));
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            // fetch only the users for the current tenant
            return AsSingleTenant(FindUsersByName("%", pageIndex, pageSize, out totalRecords));
        }

        public override string GetPassword(string username, string passwordAnswer)
        {
            return base.GetPassword(AsMultiTenant(username), passwordAnswer);                
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            return AsSingleTenant(base.GetUser(AsMultiTenant(username), userIsOnline));
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            return AsSingleTenant(base.GetUser(providerUserKey, userIsOnline));
        }

        public override string GetUserNameByEmail(string email)
        {
            return base.GetUserNameByEmail(email);
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            base.Initialize(name, config);
        }

        public override string ResetPassword(string username, string passwordAnswer)
        {
            return base.ResetPassword(AsMultiTenant(username), passwordAnswer);
        }

        public override bool UnlockUser(string username)
        {
            return base.UnlockUser(AsMultiTenant(username));
        }

        public override bool ValidateUser(string username, string password)        
        {
            return base.ValidateUser(AsMultiTenant(username), password);
        }

        public override void UpdateUser(MembershipUser user)
        {
            base.UpdateUser(AsMultiTenant(user));
        }

        #region private conversion helpers
        private string AsMultiTenant(string singleTenant)
        {
            return string.Format("{0}.{1}", Tenant, singleTenant);
        }

        private static string AsSingleTenant(string multiTenant)
        {
            return multiTenant.Substring(multiTenant.IndexOf('.') + 1);
        }

        private MembershipUser AsMultiTenant(MembershipUser user)
        {
            return new MembershipUser(
                user.ProviderName,
                AsMultiTenant(user.UserName),
                user.ProviderUserKey,
                AsMultiTenant(user.Email),
                user.PasswordQuestion,
                user.Comment,
                user.IsApproved,
                user.IsLockedOut,
                user.CreationDate,
                user.LastLoginDate,
                user.LastActivityDate,
                user.LastPasswordChangedDate,
                user.LastLockoutDate);
        }

        private static MembershipUser AsSingleTenant(MembershipUser user)
        {
            return new MembershipUser(
                user.ProviderName,
                AsSingleTenant(user.UserName),
                user.ProviderUserKey,
                AsSingleTenant(user.Email),
                user.PasswordQuestion,
                user.Comment,
                user.IsApproved,
                user.IsLockedOut,
                user.CreationDate,
                user.LastLoginDate,
                user.LastActivityDate,
                user.LastPasswordChangedDate,
                user.LastLockoutDate);
        }

        private static MembershipUserCollection AsSingleTenant(MembershipUserCollection users)
        {
            var sqlUsers = new MembershipUserCollection();
            foreach (MembershipUser user in users)
                sqlUsers.Add(AsSingleTenant(user));
            return sqlUsers;
        }
        #endregion
    }    
}
